RubyLLM phased migration feature-flagged - #775
Draft
fluxgravity wants to merge 7 commits into
Draft
Conversation
This was referenced Aug 24, 2026
fluxgravity
force-pushed
the
rubyllm-phased-migration-with-feature-flag
branch
from
August 27, 2026 04:20
c7a6644 to
5e39539
Compare
This was referenced Aug 27, 2026
Merged
Add ruby_llm ~> 1.16.0 gem, feature flag (use_ruby_llm, off by default), empty AIBackend::RubyLLM shell with supports_driver? returning false, and TestClient::RubyLLM double. The APIService#ai_backend guard remains dead code until Phase 2 flips supports_driver? to true. Also fix a latent bug where preference writes clobbered unrelated keys -- both UsersController and Settings::PeopleController now deep-merge incoming preferences instead of replacing the entire column. Permit feature: [:use_ruby_llm] in both preference write paths so the flag is per-user writable (no UI yet).
Implements AIBackend::RubyLLM with streaming, token counting, oneoff
messages, and error mapping for the openai driver. RubyLLM gem's
builder-pattern Chat is used with per-request context keys.
- supports_driver? returns true for "openai" only
- get_oneoff_message and stream_next_conversation_message are full
overrides using RubyLLM's Chat API
- stream_handler proc yields chunk content, captures input/output
tokens, and maps RubyLLM errors to ConfigurationError/RateLimitError
- preceding_conversation_messages returns text-only {role, content}
hashes, skipping tool messages (tool support deferred to Phase 5)
- test_execute mirrors the existing OpenAI pattern with context key
- TestClient::RubyLLM::Chat upgraded with complete/content/ask,
class-level stubbables (text, blank_response, error_to_raise, tokens)
- 24 backend tests + 8 job integration tests
Extend AIBackend::RubyLLM to rout through all three providers (OpenAI, Anthropic, Gemini) behind the use_ruby_llm feature flag. - Add provider_slug to dynamically select provider from api_service.driver - Update ruby_llm_context to set per-provider API keys via public_send - Update build_chat to pass provider_slug instead of hardcoded :openai - Add provider_for_url for test_execute routing - Expand supports_driver? to return true for all three backends - Add Anthropic and Gemini streaming tests (backend and job level) - Fix duplicate use_ruby_llm key in options.yml from rebase - Fix upstream controller tests for nested preferences params
Extend preceding_conversation_messages to pass multimodal input to RubyLLM for all three drivers. - Build RubyLLM::Content with image attachments for messages with documents - Inline PDF text via document.extract_pdf_text (no native PDF upload) - Strip json_of_generated_image from assistant content on replay - Add per-driver vision streaming, PDF extraction, mixed image+text, and sanitize_content tests
fluxgravity
force-pushed
the
rubyllm-phased-migration-with-feature-flag
branch
from
September 1, 2026 20:32
cc20f15 to
98d5187
Compare
Implement Phase 5 of the RubyLLM migration. Closes the unknown-name gap with a new InterceptedChat (raise ToolCallIntercepted for any tool call) and maps the unified error contract at the chat.complete boundary, since RubyLLM raises in its HTTP layer outside the per-chunk stream_handler.
… (Phase 6) Enable the RubyLLM backend by default for all non-test environments via a hand-rolled ERB expression in options.yml, while keeping the test-environment default off so the legacy SDK suite keeps dispatching to the old backends. The flag stays revertible through USE_RUBY_LLM_FEATURE or a per-user preference, and the feature test now pins the boolean false default.
strivedi183
added a commit
to strivedi183/hostedgpt
that referenced
this pull request
Sep 2, 2026
The panel derives its rows from the driver enum, so OpenRouter, which rides the openai driver and dispatches by driver plus base URL, could never earn a row of its own. derived_backend_names now appends :openrouter_ai_backend after the driver-derived list, keyed by dispatch identity rather than the borrowed dialect. The choice persists through the existing settings form with no controller changes. Groq rides the openai driver too but stays out until RubyLLM support for it is verified; when it is, Groq joins the same way. Behavior window: the choice is stored and validated at this commit, but nothing reads it yet. The row's RubyLLM radio stays disabled until AllYourBot#775 teaches AIBackend::RubyLLM.supports_driver? "openrouter" and gives its URL-to-provider mapping an openrouter.ai arm, and no runtime consumer dispatches on the stored choice until AllYourBot#775's consumer resolves choices by dispatch identity, never driver alone, or ruby_llm?("openai") would silently read the wrong row. AllYourBot#775 rebases onto this series. Tests pin both the persist path (round-trip with sibling preferences intact) and the render: the row shows the OpenRouter label, the SDK radio, and a disabled RubyLLM radio; the typo guard still rejects groq_ai_backend, which has no derivation entry.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Phases 1–6 of the RubyLLM migration plan — a feature-flagged migration from three per-provider SDK gems (
ruby-openai,ruby-anthropic,gemini-ai) to the unifiedruby_llmgem. Phases 1–5 land the new backend behind a flag; Phase 6 flips the flag on by default outside the test environment. Each commit maps to one plan phase; they can be split into separate PRs later if narrower review is desired.Phase 1 — Scaffolding (
406b850)ruby_llm ~> 1.16.0(patchlocked; existing SDK gems untouched)use_ruby_llmfeature flag (off by default) and initializer with intentional dummy keysAIBackend::RubyLLMshell withsupports_driver? → falseAPIService#ai_backendguard routes through RubyLLM only when flag is on AND driver is supportedPhase 2 — OpenAI text-only chat (
a04415c)supports_driver?returns true foropenaionlyget_oneoff_message/stream_next_conversation_messagefull overrides on RubyLLM's Chat APIstream_handleryields chunks, captures tokens, maps RubyLLM errors toConfigurationError/RateLimitErrorpreceding_conversation_messages, tool messages skippedopenai_api_baseoverrideTestClient::RubyLLM::ChatfakePhase 3 — Anthropic + Gemini text chat (
2c917bc)supports_driver?now true for all three driversprovider_slug/ dynamicruby_llm_context/provider_for_urlfor per-provider routing andtest_executePhase 4 — Image/PDF attachment parity (
98d5187)RubyLLM::Content.new(text, attachments)withRubyLLM::Attachment(ActiveStorage-native)document.extract_pdf_text(no native PDF upload)sanitize_contentstripsjson_of_generated_imageon replayPhase 4.5 — Tool-interception pre-check (
c478d05)InterceptedTool < ::RubyLLM::Tool, registered perToolboxtool, whoseexecuteraisesToolCallInterceptedname/description/params_schemastored explicitly (the base class derivesnamefrom the class name, collapsing all tools into one entry)tool_interception_test.rbasserts the halt against the pinlocked gem behaviorPhase 5 — Tool/function calling parity (
1591e82)stream_next_conversation_messageextractscontent_tool_callsand formats them viaformat_tool_callsfor downstreamGetNextAIMessageJobhandlingtool_instancesmapsToolbox.tools(OpenAI-format descriptors) toInterceptedToolinstances — no schema re-derivationInterceptedChat#handle_tool_calls(private override) raisesToolCallInterceptedfor any tool call, closing the unknown/hallucinated-name gap thatInterceptedToolalone could not halt;InterceptedTool#executekept as a safety net against upstream renameget_oneoff_messagewidened with ajson:kwarg for the provider-blind autotitle path (appends a JSON-coercion instruction, not rawresponse_format:, which 400s on Anthropic)ConfigurationError < AIBackend::ConfigurationError,RATE_LIMIT_ERRORS→Faraday::TooManyRequestsError(mapped at thechat.completeboundary),RateLimitErrordroppedtools_enabled?(URL check) since the class-levelsupports_tools?gate is bypassed when the flag is onruby_llm_test.rb),get_next_ai_message_job_ruby_llm_test.rb, newautotitle_conversation_job_ruby_llm_test.rb, interception specs, and tool-call simulation inTestClient::RubyLLM::ChatPhase 6 — Flip default on, with a test-env carve-out (
eaaa250)use_ruby_llmdefault on for every non-test environment via hand-rolled ERB (ENV["USE_RUBY_LLM_FEATURE"] || (Rails.env.test? ? "false" : "true")); the test environment stays off so the legacy SDK suite keeps dispatching to the old backendsUSE_RUBY_LLM_FEATURE=false, or a per-userpreferences[:feature][:use_ruby_llm] = falsefeature_test.rbupdated to pin the env-dependent test-env default (booleanfalse) plus the user-preference overrideToolbox::Imageimage-generation migration toRubyLLM.context.paintwas intentionally skipped — image generation continues through the existing OpenAI delegationNot in this PR
No existing backends, tests, or gems are modified. Image generation still runs through the existing OpenAI client path (the optional Phase 6
RubyLLM.context.paintmigration was skipped). The old backends, old gems, old test clients, thesupports_driver?guard, and the flag itself are removed in Phase 7, which ships as a separate PR (gated on production dogfood signal from Phase 6).Testing
bin/rails test— 892 runs, 0 failures, 0 errors. New RubyLLM tests opt in viastub_features(use_ruby_llm: true). The test-environment default staysfalse(the Phase 6 carve-out), so the legacy SDK tests keep dispatching to the old backends and the suite stays green. Non-test environments now route through RubyLLM by default; the plan's remaining dogfood checks (text/images/PDFs/tools/autotitle/cancellation/rate-limit/blank-response parity across all three providers) are manual criteria to confirm before merge and before Phase 7.